# ommbot/bot.py
#
#

""" bot class """

__copyright__ = 'this file is in the public domain'

from ommbot.world import rlog
import ommbot.eventhandler
import ommbot.generic
import ommbot.pdod
import ommbot.irc
import ommbot.world
import os
import re
import socket
import time
import types
import Queue

dccchatre = re.compile('\001DCC CHAT CHAT (\S+) (\d+)\001', re.I)

class Bot(ommbot.irc.Irc):

    def __init__(self, name, datadir, owner):
        super(Bot, self).__init__()
        self.owner = str(owner)
        self.name = str(name)
        self.datadir = str(datadir)
        self.userhosts = {}
        self.state = ommbot.pdod.Pdod(datadir + os.sep + '%s.state' % 
                         self.name)
        if not self.state['channels']:
             self.state['channels'] = []
        if ommbot.world.plugs:
            self.users = ommbot.world.plugs.users
        try:
            username = self.users.getname(self.owner)
            if not username:
                merged = self.users.merge('owner', self.owner)
                if not merged:
                    self.users.add('owner', [self.owner], perms = ['USER', \
                                       'OPER'])
        except Exception, ex:
            ommbot.generic.handle_exception()

    def start(self):
        self.commandhandler = ommbot.eventhandler.Commandhandler()
        self.commandhandler.start()

    def __del__(self):
        self.save()

    def exit(self):
        self.state.save()
        quitmsg = self.state['quitmsg']
        if not quitmsg:
            quitmsg = 'http://r8.cg.nu'
        self.quit(quitmsg)
        self.stop = 1

    def save(self):
        self.state.save()

    def clone(self, old):
        self.owner = old.owner
        self.name = old.name
        self.nick = old.nick
        self.orignick = old.orignick
        self.server = old.server
        self.port = old.port
        self.ipv6 = old.ipv6
        self.sock = old.sock
        self.fsock = old.fsock
        ommbot.thr.start_new_thread(self.readloop, ())
        old.name = old.name + '.old'

    def getchannelmode(self, channel):
        self.send('MODE %s' % channel.lower())

    def setcc(self, channel, cc):
        channel = channel.lower()
        self.state.set(channel, 'cc', cc)
        self.save()

    def shutdown(self):
        self.stop = 1
        self.commandhandler.shutdown()

    def join(self, channel, pwd=None):
        channel = channel.lower()
        if pwd:
            self.state.set(channel, 'key', pwd)
        key = self.state.get(channel, 'key')
        if key:
            super(Bot, self).join(channel, key)
        else:
            super(Bot, self).join(channel)

    def joinchannels(self):
        for i in self.state['channels']:
            rlog(10, self.name, 'joining %s' % i)
            self.join(i)

    def handle_privmsg(self, ievent):
        """ see if we need to dispatch command """
        # check if its dcc chat request
        chat = re.search(dccchatre, ievent.txt)
        if chat:
            # check if the user is known
            if self.users.allowed(ievent.userhost, 'USER'):
                # start connection
                ommbot.thr.start_new_thread(self.dccconnect, (ievent.nick, \
                                            ievent.userhost, chat.group(1), \
                                            chat.group(2)))
                return
        # check if its ctcp
        if ievent.txt and ievent.txt[0] == '\001':
            self.handle_ctcp(ievent)
            return
        # check if its a /msg
        if ievent.channel == self.nick.lower():
            ievent.msg = 1
            ievent.printto = ievent.nick
            if ievent.txt[0] in ['!', '@']:
                ievent.txt = ievent.txt[1:]
            ommbot.world.plugs.trydispatch(self, ievent)
            return
        # it's not a message so set printto to channel
        ievent.printto = ievent.channel
        # check for control character
        cc = self.state.get(ievent.channel, 'cc')
        if not cc:
            cc = '!'
        if ievent.txt[0] == cc:
            ievent.txt = ievent.txt[1:]
            ommbot.world.plugs.trydispatch(self, ievent)
            return
        # see if were adressed by nick:
        txtlist = ievent.txt.split(':', 1)
        if txtlist[0].lower() == self.nick.lower():
            if len(txtlist) < 2:
                return
            ievent.txt = txtlist[1].strip()
            ievent.makeargrest()
            ommbot.world.plugs.trydispatch(self, ievent)
            return

    def handle_ievent(self, ievent):
        super(Bot, self).handle_ievent(ievent)
        if ommbot.world.plugs:
            j = ommbot.ircevent.Ircevent()
            j.copyin(ievent)
            ommbot.world.plugs.callbacks.check(self, j)

    def handle_join(self, ievent):
        """ handle joins """
        chan = ievent.channel
        nick = ievent.nick.lower()
        # see if its the bot who is joining
        if nick == self.nick.lower():
            if chan not in self.state['channels']:
                self.state['channels'].append(chan)
                self.state.save()
            self.who(chan)
            self.getchannelmode(chan)
            return
        # sync joined user with userhosts cache
        self.userhosts[nick] = ievent.userhost

    def handle_433(self, ievent):
        """ handle nick already taken """
        nick = ievent.postfix.split()[1]
        alternick = self.state['alternick']
        if alternick and not self.nickchanged:
            rlog(10, self.name, 'using alternick %s' % alternick)
            self.changenick(alternick)
            self.nickchanged = 1
            return
        randomnick = ommbot.generic.getrandomnick()
        self.changenick(randomnick)
        rlog(100, self.name, 'ALERT: nick %s already in use .. using \
randomnick %s' % (nick, randomnick))
        self.nickchanged = 1

    def handle_kick(self, ievent):
        """ handle kick event """
        nick = ievent.nick.lower()
        chan = ievent.channel.lower()
        # see if its the bot who got kicked .. if so remove from channels
        if nick == self.nick.lower():
            self.state['channels'].remove(chan)
            self.state.save()

    def handle_nick(self, ievent):
        """ update userhost cache on nick change """
        self.userhosts[ievent.txt] = ievent.userhost

    def handle_part(self, ievent):
        """ handle parts """
        chan = ievent.channel
        # see if its the bot who is parting
        if ievent.nick.lower() == self.nick.lower():
            rlog(1, self.name, 'parted channel %s' % chan)
            # remove from channels
            self.state['channels'].remove(chan)        
            self.state.save()

    def handle_mode(self, ievent):
        """ check if mode is on channel if so request channel mode """
        try:
            dummy = ievent.arguments[2]
        except IndexError:
            # channel mode change has 2 arguments
            self.getchannelmode(ievent.channel)
            
    def handle_311(self, ievent):
        """ handle 311 response .. sync with userhosts cache """
        post = ievent.postfix.split()
        userhost = "%s@%s" % (post[2], post[3])
        rlog(1, self.name, 'adding %s to userhosts: %s' % (post[1].lower(), \
userhost))
        self.userhosts[post[1].lower()] = userhost

    def handle_352(self, ievent):
        """ handle 352 response .. sync with userhosts cache """
        post = ievent.postfix.split()
        userhost = "%s@%s" % (post[2], post[3])
        rlog(1, self.name, 'adding %s to userhosts: %s' % (post[5].lower(), \
userhost))
        self.userhosts[post[5].lower()] = userhost

    def handle_353(self, ievent):
        """ handle 353 response .. see if we have ops """
        userlist = ievent.txt.split()
        for i in userlist:
            if i[0] == '@' and i[1:].lower() == self.nick.lower():
                if ievent.channel not in self.opchan:
                    self.opchan.append(ievent.channel)

    def handle_324(self, ievent):
        self.state.set(ievent.channel, 'mode', ievent.arguments[2])

    def handle_invite(self, ievent):
        """ join channel if invited by OPER """
        if self.users.allowed(ievent.userhost, ['OPER', ]):
            self.join(ievent.txt)

    def dccconnect(self, nick, userhost, addr, port):
        """ connect to dcc request from nick """
        try:
            port = int(port)
            if re.search(':', addr):
                rlog(1, self.name, 'creating ipv6 socket for dcc chat with %s'\
% nick)
                sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
                sock.connect((addr, port))
            else:
                rlog(1, self.name, 'creating ipv4 socket for dcc chat with %s'\
 % nick)
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.connect((addr, port))
        except Exception, ex:
            rlog(10 , self.name, 'dcc error: %s' % str(ex))
            return
        # were connected .. start dcc loop
        self.dodcc(sock, nick, userhost)

    def dcclisten(self, nick, userhost, channel):
        """ accept dcc chat requests """
        try:
            listenip = socket.gethostbyname(socket.gethostname())
            (port, listensock) = getlistensocket(listenip)
            ipip2 = socket.inet_aton(listenip)
            ipip = struct.unpack('>L', ipip2)[0]
            chatmsg = 'DCC CHAT CHAT %s %s' % (ipip, port)
            self.ctcp(nick, chatmsg)
            sock = listensock.accept()[0]
        except Exception, ex:
            rlog(10 , self.name, 'dcc error: %s' % str(ex))
            return
        # connected
        self.dodcc(sock, nick, userhost, channel)

    def dodcc(self, sock, nick, userhost, channel=None):
        """ loop for dcc commands """
        try:
            # send welcome message .. show list of commands for USER perms
            cmndslist = ommbot.world.plugs.cmnds.list('USER')
            cmndslist.sort()
            sock.send('Welcome to OMMBOT ' + nick + " ;]\n")
            sock.send('commands we support: ' + ' '.join(cmndslist) + '\n')
            sock.send('add chan <channel> if you want to use a channel\n')
            sock.send("control character is ! .. bot broadcast is @\n")
            sockfile = sock.makefile('r')
        except Exception, ex:
            rlog(10 , self.name, 'dcc error: %s' % str(ex))
            return
        # add joined user to the partyline
        ommbot.world.partyline.add_party(self, sock, nick)
        # big loop
        while 1:
            time.sleep(0.001)
            try:
                # read from socket
                res = sockfile.readline()
                # if res == "" than the otherside had disconnected
                if self.stop or not res:
                    rlog(1, self.name, 'closing dcc with ' + nick)
                    ommbot.world.partyline.del_party(nick)
                    return
            except Exception, ex:
                ommbot.generic.handle_exception()
                rlog(10, self.name, 'closing dcc with ' + nick)
                ommbot.world.partyline.del_party(nick)
                return
            try:
                # see if user provided channel
                res = res.strip()
                chan = ommbot.generic.checkchan(res)
                if chan != None:
                    (channel, res) = chan
                else:
                    channel = ''
                # create ircevent
                ievent = ommbot.ircevent.Ircevent()
                ievent.nick = nick
                ievent.userhost = userhost
                ievent.channel = channel
                ievent.origtxt = res
                ievent.txt = res
                ievent.cmnd = 'DCC'
                ievent.bot = self
                ievent.sock = sock
                ievent.speed = 10
                ievent.makeargrest()
                # check if its a command if so dispatch
                if ievent.txt[0] == "!":
                    ievent.txt = ievent.txt[1:]
                    if ommbot.world.plugs.trydispatch(self, ievent):
                        continue
                elif ievent.txt[0] == "@":
                    ommbot.world.partyline.say_broadcast_notself(ievent.nick, \
"[%s] %s" % (ievent.nick, ievent.txt))
                    q = Queue.Queue()
                    ievent.queues = [q]
                    ievent.txt = ievent.txt[1:]
                    ommbot.world.plugs.trydispatch(self, ievent)
                    result = ommbot.generic.waitforqueue(q)
                    if result:
                        for i in result:
                            ommbot.world.partyline.say_broadcast("[bot] %s" \
                                                                      % i)
                    else:
                        ommbot.world.partyline.say_broadcast("[bot] no result")
                    continue
                else:
                    ommbot.world.partyline.say_broadcast_notself(ievent.nick, \
                                                                 "[%s] %s" % \
                                                                 (ievent.nick,\
                                                                  ievent.txt))
                # check PRIVMSG wait
                #self.privwait.check(ievent)
            except Exception, ex:
                ommbot.generic.handle_exception()
                if type(ex) == types.ListType:
                    (errno, errstr) = ex
                else:
                    errno = 0
                    errstr = str(ex)
                if errno == 32:
                    break
        rlog(1, self.name, 'closing dcc with ' + nick)
